// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using SnapObjects.Data.Properties;

namespace SnapObjects.Data
{
    internal class SysJsonNodeReader : INodeReader
    {
        private int _rootDepth;
        private readonly bool _isArray = false;
        private bool _autoRead;
        private object _value;

        private readonly Stack<DataNodeType> _levels;
        private readonly SystJsonStreamReader _streamReader;

        public static INodeReader Parse(string raw)
        {
            if (String.IsNullOrWhiteSpace(raw))
            {
                throw new ArgumentException(Resource.DATAREADER_NOTNULL_EXCEPTION);
            }

            var jsonBytes = Encoding.UTF8.GetBytes(raw);

            return Parse(new MemoryStream(jsonBytes)).ReadAsNode();
        }

        public static INodeReader Parse(Stream stream)
        {
            if (stream == null)
            {
                throw new ArgumentNullException();
            }

            return new SysJsonNodeReader(stream);
        }

        private SysJsonNodeReader(Stream stream)
        {
            _autoRead = true;

            _streamReader = new SystJsonStreamReader(stream);

            _levels = new Stack<DataNodeType>();

            this.InitNode();
        }

        private SysJsonNodeReader(
            SystJsonStreamReader streamReader, string name, DataNodeType dataNodeType, int rootDepth)
        {

            _autoRead = true;
            _rootDepth = rootDepth;
            _streamReader = streamReader;

            _levels = new Stack<DataNodeType>();

            _levels.Push(dataNodeType);

            if (_rootDepth == 0)
            {
                _isArray = dataNodeType == DataNodeType.Array;
            }

            this.Name = name;
            this.NodeType = dataNodeType;
            this.CurrentNodeName = name;
        }

        public bool IsEnd { get; private set; }
        public string Name { get; }
        public string CurrentNodeName { get; private set; }
        public DataNodeType NodeType { get; private set; }
        public DataNodeType CurrentNodeType { get; private set; }

        public IValueReader ValueReader
        {
            get
            {
                this.CheckEnd();

                return new JValueReader(this.CurrentNodeName, _value);
            }
        }

        public bool Read()
        {
            this.CheckEnd();

            if (!_streamReader.TryRead(out var reader, _autoRead) ||
                reader.CurrentDepth <= _rootDepth)
            {
                this.IsEnd = true;

                return false;
            }

            var result = !this.IsEnd;

            switch (reader.TokenType)
            {
                case JsonTokenType.StartArray:
                case JsonTokenType.StartObject:
                {
                    _autoRead = false;

                    break;
                }
                case JsonTokenType.EndObject:
                {
                    _levels.Pop();

                    result = false;

                    break;
                }
                case JsonTokenType.EndArray:
                {
                    _levels.Pop();

                    result = false;

                    if (_isArray && _streamReader.TryRead(out reader, _autoRead))
                    {
                        _autoRead = false;

                        result = reader.TokenType != JsonTokenType.EndArray;
                    }

                    break;
                }
                case JsonTokenType.PropertyName:
                {
                    this.CurrentNodeType = DataNodeType.Value;

                    this.CurrentNodeName = reader.GetString();

                    if (!_streamReader.TryRead(out reader, _autoRead) ||
                        reader.CurrentDepth <= _rootDepth)
                    {
                        this.IsEnd = true;

                        return false;
                    }

                    _value = _streamReader.GetValue(ref reader);

                    if (reader.TokenType == JsonTokenType.StartObject ||
                        reader.TokenType == JsonTokenType.StartArray)
                    {
                        _autoRead = false;
                    }

                    break;
                }
                case JsonTokenType.Comment:
                    break;
                case JsonTokenType.String:
                case JsonTokenType.Number:
                case JsonTokenType.True:
                case JsonTokenType.False:
                case JsonTokenType.Null:
                {
                    _value = _streamReader.GetValue(ref reader);

                    this.CurrentNodeType = DataNodeType.Value;

                    break;
                }
            }

            if (_levels.Count == 0)
            {
                this.IsEnd = true;

                result = false;
            }

            return result;
        }

        public INodeReader ReadAsNode()
        {
            return this.InternalReadAsNode();
        }

        private INodeReader InternalReadAsNode()
        {
            this.CheckEnd();

            if (_streamReader.TryRead(out var reader, _autoRead))
            {
                _autoRead = true;

                var currentDepth = reader.CurrentDepth;

                if (reader.TokenType == JsonTokenType.StartObject ||
                     reader.TokenType == JsonTokenType.StartArray)
                {
                    this.CurrentNodeType = reader.TokenType == JsonTokenType.StartObject ?
                        DataNodeType.Object : DataNodeType.Array;

                    _levels.Push(this.CurrentNodeType);

                    return new SysJsonNodeReader(
                        _streamReader, this.CurrentNodeName, this.CurrentNodeType, currentDepth);
                }
                else
                {
                    throw new JsonException($"Unexpected token '{reader.TokenType}'.");
                }
            }

            return null;
        }

        public IValueReader ReadAsRaw()
        {
            this.CheckEnd();

            string value;

            var name = this.CurrentNodeName;

            _streamReader.HoldSegment();

            if (!_streamReader.TryRead(out var reader, false))
            {
                return null;
            }

            var tokenType = reader.TokenType;

            if (tokenType == JsonTokenType.StartArray || tokenType == JsonTokenType.StartObject)
            {
                _streamReader.ReadAsRaw(ref reader);

                value = Encoding.UTF8.GetString(_streamReader.GetHoldSegmentSequence().ToArray());
            }
            else
            {
                value = _value?.ToString();
            }

            _streamReader.CancelHoldSegments();

            var autoRead = _autoRead;

            _autoRead = true;

            this.Read();

            _autoRead = autoRead;

            return new JValueReader(name, value);
        }

        public void SkipCurrentNode()
        {
            this.CheckEnd();

            if (!_streamReader.Skip())
            {
                this.IsEnd = true;
            }
        }

        private void InitNode()
        {
            var reader = _streamReader.GetReader();

            if (!_streamReader.TryPreRead(ref reader))
            {
                this.IsEnd = true;

                return;
            }

            var tokenType = reader.TokenType;

            if (tokenType == JsonTokenType.None)
            {
                this.IsEnd = true;

                return;
            }

            if (tokenType == JsonTokenType.StartObject)
            {
                this.NodeType = DataNodeType.Object;
            }
            else if (tokenType == JsonTokenType.StartArray)
            {
                this.NodeType = DataNodeType.Array;
            }
            else
            {
                throw new JsonException($"Unexpected token '{(char)reader.GetByte()}'.");
            }

            _rootDepth = reader.CurrentDepth;

            this.IsEnd = false;
        }

        private void CheckEnd()
        {
            if (this.IsEnd)
            {
                throw new JsonException("The node has been read to the end.");
            }
        }
    }
}